260

     # Синтаксис xpath


Введение

Практические примеры по работе с xpath приводилсь в статье.

Xpath - активно используется в автоматизации веб-тестирования при написании UI-тестов.

Данный язык помогает перемещаться по DOM-структуре XML-файла (используется для навигации и поиска внутри xml), а также возвращает элементы (узлы XML-документа), соответствующие запросам.

Язык является РЕГИСТРОЗАВИСИМЫМ!!! Все команды всегда с маленькой буквы!


Пути в xpath

Абсолютные пути

Абсолютный xpath начинается с одного слэша ( / ) и указывает на полный путь из корневого узла (root) к целевому (target). Например, следующее выражение в XPath:

/html/body/div[1]/h1

вернёт нам все элементы типа <h1>, которые расположены по указанному пути. Из примера ниже мы получим только первый и второй элемент, но не третий:

<html>
    <head><head>
    <body>
        <div>
            <h1>Первый элемент</h1>
            <h1>Второй элемент</h1>
        </div>
        <div>
            <h1>Третий элемент</h1>
        </div>
    </body>
    <footer/>
</html>

Чтобы получить третий элемент, требуется изменить наш запрос на:

/html/body/div[2]/h1

Обращение к верхнеуровнему элементу

Запрос вида:

/html

Найдёт только элемент <html>, но не найдёт никакие другие, т. к. верхнеуровневый элемент типа <html> только один.

Если же мы хотим выйти на какие-либо дочерние элементы с помощью абсолютного пути, то требуется всегда следовать полной структуре файла от самого начала:

/html/head/meta
/html/body/div

Если мы укажем просто /head/meta, то ничего не найдётся, так как в файле нет верхнеуровнего элемента типа <head>. Главный предок всех элементов в данном случае - это <html>, поэтому при использовании абсолютного пути поиск всегда требуется начинать именно с этого элемента.


Относительные пути

Для применения относительного пути используется двойной слэш ( // ). При таком подходе указанный в запросе путь:

  • ищется в любом месте документа
  • не имеет никакого значения
    • где он распологается
    • кто у него родитель

Главное - чтобы этот адрес существовал в документе. Напр., запрос вида:

//div

вернёт абсолютно все элементы типа <div>, найденные в любом месте документа.

Чтобы найти все ссылки, расположенные внутри любого списка, можно воспользоваться синтаксисом вида:

//ul//a

Комбинирование подходов

Для поиска какого-либо элемента чаще используются комбинированные подходы. Изначально вместо главного предка документа мы отталкиваемся от какого-либо элемента, напр., <ul>

//ul

Далее, нам, например, нужны только те ссылки, которые находятся исключительно внутри элементов <div>, а у элементов <div> на любом уровне есть родитель / предок типа <ul>:

//ul//div/a

Тем самым, мы использовали:

  • //ul - чтобы найти все элементы типа <ul>
  • //div - чтобы найти только те элементы <div>, у которых есть предок типа <ul>
  • /a - чтобы найти только те элементы <a>, у которых есть родитель типа <div>

Важно понимать, что - добавляя новый элемент в xpath-путь - мы полностью меняем запрос, а с ним и возвращаемый элемент:

  • //ul - вернутся все элементы типа <ul>
  • //ul//div - вернутся все элементы типа <div>, у которых есть предок типа <ul>
  • //ul//div/a - вернутся все элементы типа <a>, у которых есть родитель типа <div>, при этом у элемента div один из предков обязательно должен быть <ul>.

Отношение элементов друг к другу

Выше уже упоминались такие понятия как предок, родитель, дочерние элементы (чайлд). Именно они и будут рассмотрены далее.


Родитель

Родитель - это первый вышестоящий элемент от рассматриваемого. Пример:

<html>
    <head><head>
    <body>
        <div id="par_1">
            <h1>Первый элемент</h1>
            <h2>Второй элемент</h1>
        </div>
        <div>
            <h3>Третий элемент</h1>
        </div>
    </body>
    <footer/>
</html>

У всех трёх элемнтов типа <h*> родителями являются элементы div. Только это два разных div-а, то есть, два разных родителя:

  • h1, h2 имеют родителя <div id="par_1">
  • h3 имеет в качестве родителя элемент <div>

У элементов <div id="par_1"> и <div> родителем является элемент <body>.

У элементов <head>, <body>, <footer> родителем является элемент <html>


Предок

Предок - это любые вышестоящие элементы в цепочке до нашего элемента, определяемые по связи типа "родитель". Напр.:

<html>
    <head><head>
    <body>
        <div id="par_1">
            <h1>Первый элемент</h1>
            <h2>Второй элемент</h1>
        </div>
        <div>
            <h3>Третий элемент</h1>
        </div>
    </body>
    <footer/>
</html>

У элементов <h1> и <h2> имеются:

  • один родитель (<div id="par_1">)
  • два предка (<body>, <html>)

У элемента <h1> имеется: - один родитель (<div>) - те же предки, что и у элементов <h1> и <h2>


Дочерние элементы и наследники

<html>
    <head><head>
    <body>
        <div id="par_1">
            <h1>Первый элемент</h1>
            <h2>Второй элемент</h1>
        </div>
        <div>
            <h3>Третий элемент</h1>
        </div>
    </body>
    <footer/>
</html>

Дочерними элементами для <html> являются:

  • <head>
  • <body>
  • <footer>

Если следовать логике здравого смысла, то абсолютно все элементы, находящиеся по структуре ниже <html>, по сути тоже являются дочерними.

Но - чтобы не путаться - под дочерними будем понимать только "первую очередь" элементов, то есть, только те, которые находятся на один уровень ниже.

Те же элементы - которые имеют "вложенность" более одного уровня - будем называть наследниками. Тем самым у элемента <html> имеется:

  • три дочерних элемента: - <head>, <body>, <footer>
  • и много наследников: <div id="par_1">, <div>, <h1>, <h2>, <h3>

Альтернативные названия:

  • дочерний элемент: чайлд, чайлды, чилдрен
  • потомок: наследник

Практические примеры поиска

Xpath имеет мощнейщий синтаксис для поиска элементов, с помощью которого можно создать селектор абсолютно на любой элемент. Поможет в этом совместное использование:

  • предикатов
  • атрибутов
  • xpath-функций
  • логических операторов
  • осей xpath
  • специального синтаксиса для перемещения между элементами

Применение предикатов

Использование атрибутов

Под атрибутами подразумевается любое ключевое слово, хранимое внутри элемента, напр.:

<table width="100%" style="background: #3f4137; padding: 10px 5px 10px 5px">
<div id="previous_pages" onclick="show_page_by_button(-10)" style="text-align: center;">
<label style="font-size: 16px;">
<input id="current_page" type="text" size="3" value="1" style="text-align: center; font-size: 16px;">
<label id="max_page" style="font-size: 16px; color: white">

У первого элемента из примера выше (table) есть атрибуты width и style. У второго - id, onclick, style. И так далее.

Эти атрибуты можно и нужно использовать для более точного определения элемента.

Так, например, если на главной странице сайта https://g-oak.ru воспользоваться xpath-ом типа:

//table

То будет найдено 11 таблиц. Чтобы получить доступ к одной, конкретной из них, нужно проанализировать, чем она отличается от других. Таблица, которая нам требуется, единственная из всех содержит значение width="100%".

Получается, для обращения к ней будет достаточно использовать запрос типа:

//table[@width="100%"]

тем самым, отфильтровав все остальные таблицы и получив уникальный, тот самы элемент.

Синтаксис обращения к атрибуту элемента выглядит следующим образом:

//элемент[@атрибут="значение атрибута"]

Другие примеры:

//div[@id="previous_pages"]
//input[@id="current_page"]
//label[@id="max_page"]

Поиск по номеру элемента

Другим способом выбора элемента является обращение к элементу по номеру. Но нужно понимать, что такой подход работает, только если элементы находятся на одном уровне. Рассмотрим пример:

<body>
    <div>1</div>
    <div>
        <label value="10">1
            <img src="1.png">
        </label>
        <label id="important">2</label>
    </div>
    <div>3</div>

Чтобы вернуть только второй label можно воспользоваться любым из следующих вариантов:

//label[2]
//*/label[2]
//body/*/label[2]
//div/label[2]
и т. д.

Но! Стоит изменить структуру, напр., на такую:

<body>
    <div>1</div>
    <div>
        <label value="10">1
            <img src="1.png">
        </label>
    </div>
    <div>
        <label id="important">2</label>
    </div>

И больше по запросу //label[2] у нас ничего не найдётся. Дело в том, что данной командой мы говорим:

  • верни нам любой второй label
  • который находится внутри одного родителя

А таких элементов в этом случае у нас нет, т. к. оба лейбла являются первыми элементами и их можно найти следующими запросами:

//label
//label[1]

Важно помнить, что отсчёт ведётся именно от родителя (внутри одного узла).


Подробнее о предикатах

Рассмотренные выше способы на умном языке называются термином предикаты xpath.

Они дают дополнительные возможности для поиска элементов:

[1] - указание номера элемента (начинаются с 1)
@ - использовать какой-либо аттрибут элемента
    - `>` - больше
    - `<` - меньше
    - `>=` - больше или равно
    - `<=` - меньше или равно
    - `=` - равно
    - `!=` - не равно

Особое внимание стоит обратить на возможности сравнения. Зачастую используется именно знак равно и забываются остальные варианты.

Чтобы вернуть лейбл из примера выше, мы можем исключить второй (ненужный) лейбл следуюшим образом:

//label[@id!="important"]

Тем самым нам вернётся первый лейбл.

Примеры ниже ни к чему не относятся, просто показывают синтаксис:

//label[@id="important"]
//label[@value>=10]
//label[@value>=10]/img

Использование функций xpath

Чтобы писать более сложные xpath, также потребуется изучать его функции:

  • text()
  • contains()
  • starts-with()
  • ends-with() - не использовать (доступен только в xpath 2.0, не реализован в большинстве браузеров)

Главное в их применении - это запомнить особенности синтаксиса.


text()

Предположим у нас есть элемент вида:

<label id="index_title"># Синтаксис xpath</label>

Да, мы его можем найти по уникальному id:

//label[@id="index_title"]

Но так как в данной главе мы изучаем функцию text(), то будем его искать следующим образом:

//label[text()="# Синтаксис xpath"]

Запоминаем, что это функция, поэтому:

  • не надо ставить никаких @ перед ней
  • после слова text идут скобки ()

К сожалению, у этой функции имеется своя специфика в поведении. Следующий элемент не удастся найти с помощью ранее указанного шаблона:

<label id="index_title"># Синтаксис xpath
</label>

Причина в разрыве строки между словами xpath </label>, поэтому - в качестве решения - пользуемся функцией contains().


contains()

Функция contains является более широкой чем text() и принимает на вход два аргумента:

//элемент[contains(арг1, арг2)]

Запоминаем:

  • contains() прописывается внутри квадратных скобок
  • принимает через запятую два аргумента на вход, где:
    • arg_1 - это где ищем текст
    • arg_2 - какой текст
  • в качестве arg_1
    • может выступать как функция (text())
    • так и атрибут элемента (объявляется через символ @)
  • аргументы отделяются друг от друга через запятую
    • не надо пытаться писать сюда никаких = и пр.

contains() + text()

Как уже объяснялось выше, функция text() не очень хорошо работаем с примером ниже:

<label id="index_title"># Синтаксис xpath
</label>

Но данная проблема легко решается с помощью следующего запроса:

//label[contains(text(), "# Синтаксис xpath")]

contains() + атрибуты

Функция contains() может использоваться также для поиска части текста в абсолютно любом атрибуте.

Допустим, у нас имеется элемент:

<div id="previous_pages" onclick="show_page_by_button(-10)" style="text-align: center; padding: 2px; border: 1px solid yellow; margin: 2px; border-radius: 5px; background: lightgray; color: rgb(249, 249, 249); display: flex; align-items: center; justify-content: center;">

и мы хотим найти его по части атрибута style, а именно по тексту background: lightgray.

Для этого пишем запрос вида:

//div[contains(@style, "background: lightgray")]

Также мы его можем найти и по любому другому атрибуту:

//div[contains(@id, "previous")]
//div[contains(@onclick, "utton(-10)")]

starts-with()

Все моменты, которые необходимо было озвучить, уже упомянуты в главе contains(). Функция starts-with() ничем не отличается от contains() в плане синтаксиса.

Единственная разница состоит в механизме поиска:

  • если contains() ищет любое совпадение части текста в любом месте
  • то starts-with() ищет элементы, которые начинаются с переданного текста

Использование с функцией text()

<label id="index_title"># Синтаксис xpath
</label>

//label[starts-with(text(), "# Син")]

Использование с атрибутом

`<div id="previous_pages" onclick="show_page_by_button(-10)">`

//div[starts-with(@onclick, "show_pag")]

ends-with()

Доступен только в xpath 2.0, не реализован в большинстве браузеров. В качестве решения можно использовать css-селекторы => span[id$='конец_текста']


Использование логических операторов

В XPath имеются логические операторы andor и not, для указания нескольких условий поиска и сочетания условий. Они РЕГИСТРОЗАВИСИМЫ, использовать только строчное написание.


and - совпадение двух условий

<li class="article_entry">
//li[contains(@class, "article") and contains(@class, "_entry")]

<div id="previous_page" onclick="show_page_by_button(-1)">
//div[@id="previous_page" and starts-with(@onclick, "show")]

or - совпадение одного из условий

//div[@id="previous_page" or @id="previous_pages"]
//div[contains(@id, "pages") or starts-with(@id, "previous")]
//div[contains(@id, "pa") or contains(@id, "ges") or starts-with(@id, "previous")]

not

Отрицание not имеет несколько случаев применения, а именно:

  • проверка на отсутствие атрибута
  • отрицание функции или проверки атрибута

проверка отсутствия атрибута

Элемент, у которого есть атрибута style:

//div[@style]

Элемент, у которого нет атрибута style:

//div[not(@style)]

отрицание условия атрибута

Все элементы div, у которых нет атрибута id равного "background_main"

//div[not(@id="background_main")]

Все элементы div, у которых есть атрибут id, но он не равен "background_main"

//div[@id!="background_main"]

отрицание функции

//li[not(contains(@class, "article"))]
//table[not(starts-with(@id, "table_"))]

Логическое объединение условий

Для объединения нескольких условий их можно заключать в скобки, используя символический синтаксис, показанный далее:

//*[(... and ...) or ...]
//*[((... and ...) or ...) and ...]
//*[not((... and ...) or ...) or ...]

Навигация по DOM-структуре

В этой части главы будут рассмотрены дополнительные способы "переключения" между элементами.

Использование осей xpath

Оси — это база языка xpath. Выделяют следующие оси:

  • parent
  • ancestor
  • ancestor-or-self
  • child
  • descendant
  • following
  • following-sibling
  • preceding
  • preceding-sibling

Ранее уже описывались некоторые из них, как например:

  • parent - родитель
  • ancestor - предки
  • child - дочерние элементы
  • descendant - потомки

Данные оси являются ключевыми словами, использующимися в xpath-запросах. Они представляют из себя дополнительный механизм поиска элементов и позволяют более гибко переключаться между элементами.

Допустим, нам нужен локатор для определённого элемента, у которого нет никакого уникального атрибута и за него никак не зацепиться. Но при этом у него есть чайлд с уникальным id. Тогда мы можем написать xpath для чайлда и, отталкиваясь от него, легко выйти на родителя - то есть, изначально искомый нами элемент. Подробнее в примерах ниже.


parent

Ось parent (родитель) позволяет выбрать родителя какого-либо элемента:

<div id='body'>
    <div id='A1'></div>
    <div id='A2'>
        <div id='B1'></div>
    </div>
    <div id='A3'></div>
</div>

При работе с осями необходимо учитывать особый синтаксис, а именно:

  • использовать одинарный слеш (абсолютный путь) перед названием оси
  • использовать двойной знак двоеточия после названия оси
  • а также прописывать тип верхнеуровнего элемента
    • в данном случае родителем является div
    • если мы не знаем, какой тип имеет родитель, то можно использовать знак *
//div[@id='B1']/parent::div
//div[@id='B1']/parent::*
# => A1

ancestor

Ось ancestor (предок) позволяет выбрать всех предков какого-либо элемента, включая родителя, так как родитель тоже относится к предкам.

<div id='body'>
    <div id='A1'></div>
    <div id='A2'>
        <div id='B1'></div>
    </div>
    <div id='A3'></div>
</div>

Предками элемента //div[@id='B1'] являются элементы: <div id='A2'>, body

//div[@id='B1']/ancestor::*

Также нам ничто не мешает использовать и далее использовать синтаксис xpath для уточнения выбора предка:

//div[@id='B1']/ancestor::*
# div id=body, div id=A2

//div[@id='B1']/ancestor::div
# div id=body, div id=A2

//div[@id='B1']/ancestor::div[@id="body"]
# div id=body

//div[@id='B1']/ancestor::*[@id="body"]
# div id=body

То есть, после применения ancestor нам доступны все предки и мы можем выбрать из них конкретный элемент (и только из них).


ancestor-or-self

Пример синтаксиса:

//div[@id='B1']/ancestor-or-self::*
# div id=body, div id=A2, div id=B1

Делает то же самое, что и ancestor:

  • включает родителя
  • включает всех предков

с той лишь разницей, что "в цепочку предков" добавляется и сам искомый элемент. В примере выше - это <div id='B1'>.


child

<div id='body'>
    <div id='A1'></div>
    <div id='A2'>
        <div id='B1'></div>
    </div>
    <div id='A3'></div>
</div>

Child (дочерний) описывался ранее в статье. Ось child позволяет выбрать только все ПЕРВЫЕ дочерние элементы искомого узла (чайлды чайлдов недоступны).

//div[@id='body']/child::*
# => A1, A2, A3

descendant

Ось descendant (потомок) целиком возвращает абсолютно все вложенные элементы всех своих чайлдов абсолютно на всех уровнях.

<div id='body'>
    <div id='A1'></div>
    <div id='A2'>
        <div id='B1'></div>
    </div>
    <div id='A3'></div>
</div>

Потомками элемента div id='body'

//div[@id='body']/descendant::*

являются все вложенные в него элементы: A1, A2, B1, A3

Выбор конкретного потомка из всех потомков:

//div[@id='body']/descendant::div[@id="A2"]

A2


Проверка структуры кода

Следующие оси возвращают результаты в зависимости от структуры дерева:

  • following
  • following-sibling
  • preceding
  • preceding-sibling

Принцип их работы заключается в проверке, выше или ниже в структуре дерева / кода страницы находится элемент.


following

Ось following (следующий) возвращает все элементы, находящиеся ниже искомого по структуре дерева, исключая своих потомков, но включая всех чужих потомков. Поиск осуществляется по всему документу, а не только по текущей ноде.

<div id='body'>
    <div id='A1'></div>
    <div id='A2'>
        <div id='B1'></div>
    </div>
    <div id='A3'></div>
</div>

Элемент A1 находится выше всех (в ноде <div id='body'>):

//div[@id="A1"]/following::*

Поэтому в качестве результата вернутся все нижестоящие элементы: A2, B1 (потомок A2), A3.

Если же проделать то же самое применительно к A2:

//div[@id="A2"]/following::*

То вернётся только элемент A3. Элемент B1 хоть и находится ниже по коду, но он является потомком искомого элемента, поэтому не учитывается.

Если применить эту ось к элементу A3:

//div[@id="A3"]/following::*

то не вернётся ничего, т. к. это самый нижний элемент, далее по коду ничего нет.


preceding

Ось preceding (предшествующий) является противоположностью оси following, то есть меняется направление поиска.

//div[@id="A3"]/preceding::*

Вернёт узлы: B1, A2, A1, так как поиск идёт от искомого элементу вверх по структуре документа.


following-sibling

Ось following-sibling (следующий братский) возвращает все элементы, находящиеся ниже искомого по структуре дерева:

  • исключаются абсолютно все потомки
  • возвращаются только "братские" элементы

Под "братскими" понимаются элементы, которые находятся в той же ноде, то есть, на том же уровне вложенности.

<div id='body'>
    <div id='A1'></div>
    <div id='A2'>
        <div id='B1'></div>
    </div>
    <div id='A3'></div>
</div>

На запрос вида:

//div[@id="A1"]/following-sibling::*

результатом будут только элементы: A2, A3.


preceding-sibling

Ось preceding-sibling (предшествующий братский) возвращает все элементы, находящиеся выше искомого по структуре дерева:

  • исключаются абсолютно все потомки
  • возвращаются только "братские" элементы

Под "братскими" понимаются элементы, которые находятся в той же ноде, то есть, на том же уровне вложенности.

//div[@id='A3']/preceding-sibling*
# => A2, A1

Прочие способы навигации

Помимо ранее упомянутых возможностей перемещения по структуре дерева:

  • абсолютный путь ( / )
  • относительный путь ( // )
  • оси xpath

имеются и другие возможности, а именно:

  • * - любой элемента
  • /.., /../., /.././* - перейти на уровень выше (обращение к родителю)
  • .// - поиск по контексту текущего узла

Примеры применения:

//*[@class="article_entry"]/.././li[2]

Находим любой элемент класса "article_entry", поднимаемся к его родителю, от родителя берём второй элемент li. То же самое можно реализовать с помощью оси parent:

//*[@class="article_entry"]/parent::*/li[2]

Отобразить все дочерние элементы только типа li, внутри списка:

//ul[@id="list-posts"]/./li
//ul[@id="list-posts"]/li
//ul[@id="list-posts"]/child::li
//li/parent::*[@id="ul_mainmenu"]/li

Вернуть не только дочерние элементы, но и всех потомков только типа li, внутри списка:

//ul[@id="list-posts"]/.//li
//ul[@id="list-posts"]//li
//ul[@id="list-posts"]//child::li

Выводы

На примерах выше было показано, как широк синтаксис xpath. Есть множество разных способов - лёгких и простых - как вернуть узел или узлы документа. Преимущество xpath перед тем же css состоит в возможности перемещаться по структуре документа.

Используя CSS, можно идти только в глубину (без возможности обратиться к родительским элементам).